Skip to main content

Generators

In your coding journey you might encounter situations where you need to work with a very large set of data or a sequence that's computed on demand. For instance, you may be processing a dataset that's simply too large to fit into memory, or perhaps your generating a sequence where each element is the result of a computation that you only need one at a time. Typical container objects like lists are not good at these, as it requires all data to be stored in memory at once.

Python offers a powerful solution to this problem- generators.

Generators allow you to create an iterable object (called a generator iterator) like a list, but with two key differences-

  • Its values don't exist until the generator object creates it.
  • Generators 'yield' values on demand- allowing you to pause and resume execution as needed.

This 'lazy' generation method makes generators incredibly memory efficient and perfect for processing large datasets or infinite sequences.

The syntax for creating a generator is similar to defining a regular function, but with one key difference—the use of the yield keyword:

def generator():
yield something

Since generators return a generator iterator, calling them doesn’t do anything besides generate its iterator.

def simple_generator(n):
for i in range(n):
yield i

simple_generator(5) #Returns <generator object simple_generator at 0x7b1ab768c0b0>

So like other iterators, you can type convert generators to generate its contents-

list(simple_generator(5))  #Returns [0, 1, 2, 3, 4]
tuple(simple_generator(5)) #Returns (0, 1, 2, 3, 4)
set(simple_generator(5)) #Returns {0, 1, 2, 3, 4}

You can also assign the generator to a variable and access it one by one to receive a value-

def simple_generator(n):
for i in range(n):
yield i

# Create a generator object
generated=simple_generator(3)

# Get the first value
print(next(generated)) # Outputs 0

# Get the second value
print(next(generated)) # Outputs 1

# Get the third value
print(next(generated)) # Outputs 2

print(next(generated)) # Outputs StopIteration Error because the iterator has been exhausted.

Example Usages

This flexibility can be used in a variety of ways. Here's two examples-

Creating Infinite Fibonacci Numbers

def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b

# Example usage
fib = fibonacci()
for _ in range(10): # Get first 10 Fibonacci numbers
print(next(fib)) # Outputs 0,1,1,2,3,5,8,13,21,34

Creating a Counter Generator

def counter(start=0):
n = start
while True:
yield n
n += 1

# Example usage
count = counter(1)
for _ in range(5):
print(next(count)) #Outputs 1,2,3,4,5

Conclusion

Generators are well suited for these examples because we want a way to generate the values on demand rather than having them generated and stored upfront. This "on-demand" generation is especially useful in cases where the total number of items isn't known ahead of time, or it's impractical to hold all items in memory due to size. In the case of the Fibonacci sequence, for instance, we can theoretically generate an infinite number of values, something that would be impossible with a regular list without consuming all available memory. Similarly, the counter can go on indefinitely, providing unique identifiers or sequence numbers for as long as needed.

Generators offer a powerful, efficient, and elegant way to generate large sequences of data, especially when you need to handle potentially infinite series, perform computations on the fly, or manage large data sets with minimal memory usage. Whether you're iterating through numbers, processing streaming data, or generating values according to a specific algorithm, generators can be an invaluable tool in your Python programming toolkit.